/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
/*
* Created on Dec 10, 2003
* Author: atotic
*/
package org.python.pydev.editor.autoedit;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IAutoEditStrategy;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextSelection;
import org.python.pydev.core.IIndentPrefs;
import org.python.pydev.core.docutils.ImportsSelection;
import org.python.pydev.core.docutils.NoPeerAvailableException;
import org.python.pydev.core.docutils.ParsingUtils;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.core.docutils.PySelection.LineStartingScope;
import org.python.pydev.core.docutils.PythonPairMatcher;
import org.python.pydev.core.docutils.StringUtils;
import org.python.pydev.core.docutils.SyntaxErrorException;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.actions.PyAction;
import org.python.pydev.plugin.PydevPlugin;
import com.aptana.interactive_console.console.ui.internal.IHandleScriptAutoEditStrategy;
import com.aptana.shared_core.string.FastStringBuffer;
import com.aptana.shared_core.structure.Tuple;
import com.aptana.shared_core.utils.DocCmd;
/**
* Class which implements the following behaviors:
* - indenting: after 'class' or 'def'
* - replacement: when typing colons or parentheses
*
* This class uses the org.python.pydev.core.docutils.DocUtils class extensively
* for some document-related operations.
*/
public final class PyAutoIndentStrategy implements IAutoEditStrategy, IHandleScriptAutoEditStrategy {
private IIndentPrefs prefs;
private boolean blockSelection;
public PyAutoIndentStrategy() {
}
public void setIndentPrefs(IIndentPrefs prefs) {
this.prefs = prefs;
}
public IIndentPrefs getIndentPrefs() {
if (this.prefs == null) {
if (PydevPlugin.getDefault() == null) {
this.prefs = new TestIndentPrefs(true, 4);
} else {
this.prefs = new DefaultIndentPrefs(); //create a new one (because each pyedit may force the tabs differently).
}
}
return this.prefs;
}
/**
* Set indentation automatically after newline.
*
* @return tuple with the indentation to be set and a boolean determining if it was found
* to be within a parenthesis or not.
*/
private Tuple<String, Boolean> autoIndentNewline(IDocument document, int length, String text, int offset)
throws BadLocationException {
if (offset > 0) {
PySelection selection = new PySelection(document, offset);
String lineWithoutComments = selection.getLineContentsToCursor(true, true);
Tuple<Integer, Boolean> tup = determineSmartIndent(offset, document, prefs);
int smartIndent = tup.o1;
boolean isInsidePar = tup.o2;
if (lineWithoutComments.length() > 0) {
//ok, now let's see the auto-indent
int curr = lineWithoutComments.length() - 1;
char lastChar = lineWithoutComments.charAt(curr);
//we dont want whitespaces
while (curr > 0 && Character.isWhitespace(lastChar)) {
curr--;
lastChar = lineWithoutComments.charAt(curr);
}
//we have to check if smartIndent is -1 because otherwise we are inside some bracket
if (smartIndent == -1 && !isInsidePar && StringUtils.isClosingPeer(lastChar)) {
//ok, not inside brackets
PythonPairMatcher matcher = new PythonPairMatcher(StringUtils.BRACKETS);
int bracketOffset = selection.getLineOffset() + curr;
IRegion region = matcher.match(document, bracketOffset + 1);
if (region != null) {
if (!PySelection.endsInSameLine(document, region)) {
//we might not have a match if there is an error in the program...
//e.g. a single ')' without its counterpart.
int openingBracketLine = document.getLineOfOffset(region.getOffset());
String openingBracketLineStr = PySelection.getLine(document, openingBracketLine);
int first = PySelection.getFirstCharPosition(openingBracketLineStr);
String initial = getCharsBeforeNewLine(text);
text = initial + openingBracketLineStr.substring(0, first);
return new Tuple<String, Boolean>(text, isInsidePar);
}
}
} else if (smartIndent == -1 && lastChar == ':') {
//we have to check if smartIndent is -1 because otherwise we are in a dict
//ok, not inside brackets
text = indentBasedOnStartingScope(text, selection, false);
return new Tuple<String, Boolean>(text, isInsidePar);
}
}
String trimmedLine = lineWithoutComments.trim();
if (smartIndent >= 0
&& (StringUtils.hasOpeningBracket(trimmedLine) || StringUtils.hasClosingBracket(trimmedLine))) {
return new Tuple<String, Boolean>(makeSmartIndent(text, smartIndent), isInsidePar);
}
//let's check for dedents...
if (PySelection.startsWithDedentToken(trimmedLine)) {
if (lineWithoutComments.endsWith("\\")) {
//Okay, we're in something as return \, where the next line will be part of this statement, so, don't really
//go back an indent, but go up an indent.
return new Tuple<String, Boolean>(text + prefs.getIndentationString(), isInsidePar);
}
return new Tuple<String, Boolean>(dedent(text), isInsidePar);
}
boolean indentBasedOnStartingScope = false;
try {
if (PySelection.containsOnlyWhitespaces(selection.getLineContentsFromCursor())) {
indentBasedOnStartingScope = true;
}
} catch (BadLocationException e) {
//(end of the file)
indentBasedOnStartingScope = true;
}
if (indentBasedOnStartingScope) {
String lineContentsToCursor = selection.getLineContentsToCursor();
String trimmed = lineContentsToCursor.trim();
if (trimmed.length() == 0) {
return new Tuple<String, Boolean>(indentBasedOnStartingScope(text, selection, false), isInsidePar);
} else {
boolean endsWithTrippleSingle = trimmed.endsWith("'''");
if (endsWithTrippleSingle || trimmed.endsWith("\"\"\"")) {
//ok, as we're out of a string scope at this point, this means we just closed a string, so,
//we should go back to indent based on starting scope.
if (endsWithTrippleSingle) {
int cursorLine = -1;
try {
ParsingUtils parsingUtils = ParsingUtils.create(selection.getDoc(), true);
int cursorOffset = selection.getAbsoluteCursorOffset();
char c;
do {
cursorOffset--;
c = parsingUtils.charAt(cursorOffset);
} while (Character.isWhitespace(c));
int startOffset = parsingUtils.eatLiteralsBackwards(null, cursorOffset);
cursorLine = selection.getLineOfOffset(startOffset);
} catch (Exception e) {
//may throw error if not balanced or if the char we're at is not a ' or "
}
if (cursorLine == -1) {
cursorLine = selection.getCursorLine();
}
return new Tuple<String, Boolean>(indentBasedOnStartingScope(text, new PySelection(
selection.getDoc(), cursorLine, 0), false), isInsidePar);
}
}
}
}
}
return new Tuple<String, Boolean>(text, false);
}
/**
* @return the text for the indent
*/
private String indentBasedOnStartingScope(String text, PySelection selection, boolean checkForLowestBeforeNewScope) {
LineStartingScope previousIfLine = selection.getPreviousLineThatStartsScope();
if (previousIfLine != null) {
String initial = getCharsBeforeNewLine(text);
if (previousIfLine.lineWithDedentWhileLookingScope == null) { //no dedent was found
String indent = PySelection.getIndentationFromLine(previousIfLine.lineStartingScope);
if (checkForLowestBeforeNewScope && previousIfLine.lineWithLowestIndent != null) {
indent = PySelection.getIndentationFromLine(previousIfLine.lineWithLowestIndent);
text = initial + indent;
} else {
text = initial + indent + prefs.getIndentationString();
}
} else { //some dedent was found
String indent = PySelection.getIndentationFromLine(previousIfLine.lineWithDedentWhileLookingScope);
String indentationString = prefs.getIndentationString();
final int i = indent.length() - indentationString.length();
if (i > 0 && indent.length() > i) {
text = (initial + indent).substring(0, i + 1);
} else {
text = initial; // this can happen if we found a dedent that is 1 level deep
}
}
}
return text;
}
/**
* Returns the first offset greater than <code>offset</code> and smaller than
* <code>end</code> whose character is not a space or tab character. If no such
* offset is found, <code>end</code> is returned.
*
* @param document the document to search in
* @param offset the offset at which searching start
* @param end the offset at which searching stops
* @return the offset in the specified range whose character is not a space or tab
* @exception BadLocationException if position is an invalid range in the given document
*/
private int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException {
while (offset < end) {
char c = document.getChar(offset);
if (c != ' ' && c != '\t') {
return offset;
}
offset++;
}
return end;
}
private void autoIndentSameAsPrevious(IDocument d, DocumentCommand c) {
String txt = autoIndentSameAsPrevious(d, c.offset, c.text, true);
if (txt != null) {
c.text = txt;
}
}
/**
* Copies the indentation of the previous line.
*
* @param d the document to work on
* @param text the string that should added to the start of the returned string
* @param considerEmptyLines whether we should consider empty lines in this function
* @param c the command to deal with
*
* @return a string with text+ the indentation found in the previous line (or previous non-empty line).
*/
private String autoIndentSameAsPrevious(IDocument d, int offset, String text, boolean considerEmptyLines) {
if (offset == -1 || d.getLength() == 0)
return null;
try {
// find start of line
IRegion info = d.getLineInformationOfOffset(offset);
String line = d.get(info.getOffset(), info.getLength());
if (!considerEmptyLines) {
int currLine = d.getLineOfOffset(offset);
while (PySelection.containsOnlyWhitespaces(line)) {
currLine--;
if (currLine < 0) {
break;
}
info = d.getLineInformation(currLine);
line = d.get(info.getOffset(), info.getLength());
}
}
int start = info.getOffset();
// find white spaces
int end = findEndOfWhiteSpace(d, start, offset);
FastStringBuffer buf = new FastStringBuffer(text, end - start + 1);
if (end > start) {
// append to input
buf.append(d.get(start, end - start));
}
return buf.toString();
} catch (BadLocationException excp) {
// stop work
return null;
}
}
/**
* @param document
* @param length
* @param text
* @return
*/
private boolean isNewLineText(IDocument document, int length, String text) {
return length == 0 && text != null && AbstractIndentPrefs.endsWithNewline(document, text) && text.length() < 3; //could be \r\n
}
private String dedent(String text) {
String indentationString = prefs.getIndentationString();
int indentationLength = indentationString.length();
int len = text.length();
if (len >= indentationLength) {
text = text.substring(0, len - indentationLength);
}
return text;
}
private static Tuple<String, Integer> removeFirstIndent(String text, IIndentPrefs prefs) {
String indentationString = prefs.getIndentationString();
if (text.startsWith(indentationString)) {
return new Tuple<String, Integer>(text.substring(indentationString.length()), indentationString.length());
}
return new Tuple<String, Integer>(text, 0);
}
/**
* @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(IDocument, DocumentCommand)
*/
public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
if (blockSelection) {
//in block selection, leave all as is and just change tabs/spaces.
getIndentPrefs().convertToStd(document, command);
return;
}
char c;
if (command.text.length() == 1) {
c = command.text.charAt(0);
} else {
c = '\0';
}
String contentType = ParsingUtils.getContentType(document, command.offset);
switch (c) {
case '"':
case '\'':
handleLiteral(document, command, contentType.equals(ParsingUtils.PY_DEFAULT), c);
return;
}
// super idents newlines the same amount as the previous line
final boolean isNewLine = isNewLineText(document, command.length, command.text);
if (!contentType.equals(ParsingUtils.PY_DEFAULT)) {
//the indentation is only valid for things in the code (comments should not be indented).
//(that is, if it is not a new line... in this case, it may have to be indented)
if (!isNewLine) {
//we have to take care about tabs anyway
getIndentPrefs().convertToStd(document, command);
return;
} else {
if (!contentType.equals(ParsingUtils.PY_COMMENT)) {
//within string, just regular indent...
autoIndentSameAsPrevious(document, command);
return;
}
}
}
try {
if (isNewLine) {
customizeNewLine(document, command);
getIndentPrefs().convertToStd(document, command);
return;
}
if (c == '\0') {
//In some paste with more contents (c was not set), just convert tabs/spaces and go on...
getIndentPrefs().convertToStd(document, command);
return;
}
if (c == '\t') {
handleTab(document, command);
getIndentPrefs().convertToStd(document, command);
return;
}
getIndentPrefs().convertToStd(document, command);
switch (c) {
case '[':
case '{':
if (prefs.getAutoParentesis()) {
PySelection ps = new PySelection(document, command.offset);
char peer = StringUtils.getPeer(c);
if (shouldClose(ps, c, peer)) {
command.shiftsCaret = false;
command.text = c + "" + peer;
command.caretOffset = command.offset + 1;
}
}
return;
case '(':
handleParens(document, command, prefs);
return;
case ':':
/*
* The following code will auto-replace colons in function
* declaractions
* e.g.,
* def something(self):
* ^ cursor before the end colon
*
* Typing another colon (i.e, ':') at that position will not insert
* another colon
*/
if (prefs.getAutoColon()) {
performColonReplacement(document, command);
}
/*
* Now, let's also check if we are in an 'else:' or 'except:' or 'finally:' that must be dedented in the doc
*/
autoDedentAfterColon(document, command, prefs);
return;
case ' ':
/*
* this is a space... so, if we are in 'from xxx ', we may auto-write
* the import
*/
if (prefs.getAutoWriteImport()) {
PySelection ps = new PySelection(document, command.offset);
String completeLine = ps.getLineWithoutCommentsOrLiterals();
String lineToCursor = ps.getLineContentsToCursor().trim();
String lineContentsFromCursor = ps.getLineContentsFromCursor();
if (completeLine.indexOf(" import ") == -1
&& StringUtils.leftTrim(completeLine).startsWith("from ")
&& !completeLine.startsWith("import ") && !completeLine.endsWith(" import")
&& !lineToCursor.endsWith(" import") && !lineContentsFromCursor.startsWith("import")) {
String importsTipperStr = ImportsSelection.getImportsTipperStr(lineToCursor, false).importsTipperStr;
if (importsTipperStr.length() > 0) {
command.text = " import ";
}
}
}
/*
* Now, let's also check if we are in an 'elif ' that must be dedented in the doc
*/
autoDedentElif(document, command, getIndentPrefs());
return;
case ')':
case ']':
case '}':
/*
* If the command is some kind of parentheses or brace, and there's
* already a matching one, don't insert it. Just move the cursor to
* the next space.
*/
if (prefs.getAutoBraces()) {
// you can only do the replacement if the next character already there is what the user is trying to input
if (command.offset < document.getLength()
&& document.get(command.offset, 1).equals(command.text)) {
// the following searches through each of the end braces and
// sees if the command has one of them
boolean found = false;
for (int i = 1; i <= StringUtils.BRACKETS.length && !found; i += 2) {
char b = StringUtils.BRACKETS[i];
if (b == c) {
found = true;
performPairReplacement(document, command);
}
}
}
}
return;
}
}
/*
* If something goes wrong, you want to know about it, especially in a
* unit test. If you don't rethrow the exception, unit tests will pass
* even though you threw an exception.
*/
catch (BadLocationException e) {
// screw up command.text so unit tests can pick it up
command.text = "BadLocationException";
throw new RuntimeException(e);
}
}
/**
* Called right after a '('
*/
public static void handleParens(IDocument document, DocumentCommand command, IIndentPrefs prefs)
throws BadLocationException {
/*
* Now, let's also check if we are in an 'elif ' that must be dedented in the doc
*/
autoDedentElif(document, command, prefs);
customizeParenthesis(document, command, false, prefs);
}
/**
* Called right after a ' or "
*/
private void handleLiteral(IDocument document, DocumentCommand command, boolean isDefaultContext, char literalChar) {
if (!prefs.getAutoLiterals()) {
return;
}
PySelection ps = new PySelection(document, new TextSelection(document, command.offset, command.length));
if (command.length > 0) {
try {
//We have more contents selected. Delete it so that we can properly use the heuristics.
ps.deleteSelection();
command.length = 0;
ps.setSelection(command.offset, command.offset);
} catch (BadLocationException e) {
}
}
try {
char nextChar = ps.getCharAfterCurrentOffset();
if (Character.isJavaIdentifierPart(nextChar)) {
//we're just before a word (don't try to do anything in this case)
//e.g. |var (| is cursor position)
return;
}
} catch (BadLocationException e) {
}
String cursorLineContents = ps.getCursorLineContents();
if (cursorLineContents.indexOf(literalChar) == -1) {
if (!isDefaultContext) {
//only add additional chars if on default context.
return;
}
command.text = StringUtils.getWithClosedPeer(literalChar);
command.shiftsCaret = false;
command.caretOffset = command.offset + 1;
return;
}
boolean balanced = isLiteralBalanced(cursorLineContents);
Tuple<String, String> beforeAndAfterMatchingChars = ps.getBeforeAndAfterMatchingChars(literalChar);
int matchesBefore = beforeAndAfterMatchingChars.o1.length();
int matchesAfter = beforeAndAfterMatchingChars.o2.length();
boolean hasMatchesBefore = matchesBefore != 0;
boolean hasMatchesAfter = matchesAfter != 0;
if (!hasMatchesBefore && !hasMatchesAfter) {
//if it's not balanced, this char would be the closing char.
if (balanced) {
if (!isDefaultContext) {
//only add additional chars if on default context.
return;
}
command.text = StringUtils.getWithClosedPeer(literalChar);
command.shiftsCaret = false;
command.caretOffset = command.offset + 1;
}
} else {
//we're right after or before a " or '
if (matchesAfter == 1) {
//just walk the caret
command.text = "";
command.shiftsCaret = false;
command.caretOffset = command.offset + 1;
}
}
}
/**
* @return true if the passed string has balanced ' and "
*/
private boolean isLiteralBalanced(String cursorLineContents) {
ParsingUtils parsingUtils = ParsingUtils.create(cursorLineContents, true);
int offset = 0;
int end = cursorLineContents.length();
boolean balanced = true;
while (offset < end) {
char curr = cursorLineContents.charAt(offset++);
if (curr == '"' || curr == '\'') {
int eaten;
try {
eaten = parsingUtils.eatLiterals(null, offset - 1) + 1;
} catch (SyntaxErrorException e) {
balanced = false;
break;
}
if (eaten > offset) {
offset = eaten;
}
}
}
return balanced;
}
private void handleTab(IDocument document, DocumentCommand command) throws BadLocationException {
PySelection ps = new PySelection(document, command.offset);
//it is a tab
String lineContentsToCursor = ps.getLineContentsToCursor();
int currSize = lineContentsToCursor.length();
int cursorLine = ps.getCursorLine();
//current line is empty
if (lineContentsToCursor.trim().length() == 0) {
String nextLine = ps.getLine(cursorLine + 1);
String prevLine = ps.getLine(cursorLine - 1);
boolean forceTryOnNext = false;
if (prevLine.trim().length() == 0) {
//previous line is empty, so, if the next line has contents, use it to make the match.
if (nextLine.trim().length() > 0) {
forceTryOnNext = true;
}
}
if (forceTryOnNext || nextLine.trim().startsWith("@") || ps.matchesFunctionLine(nextLine)) {
int firstCharPosition = PySelection.getFirstCharPosition(nextLine);
if (currSize < firstCharPosition) {
String txt = nextLine.substring(currSize, firstCharPosition);
//as it's the same indentation from the next line, we don't have to applyDefaultForTab.
command.text = txt;
return;
}
}
}
if (cursorLine > 0) {
//this is to know which would be expected if it was a new line in the previous line
//(so that we know the 'expected' output
IRegion prevLineInfo = document.getLineInformation(cursorLine - 1);
int prevLineEndOffset = prevLineInfo.getOffset() + prevLineInfo.getLength();
String prevExpectedIndent = autoIndentSameAsPrevious(document, prevLineEndOffset, "\n", false);
String txt = prevExpectedIndent;
Tuple<String, Boolean> prevLineTup = autoIndentNewline(document, 0, txt, prevLineEndOffset);
txt = prevLineTup.o1;
txt = txt.substring(1);//remove the newline
prevExpectedIndent = prevExpectedIndent.substring(1);
if (txt.length() > 0) {
//now, we should not apply that indent if we are already at the 'max' indent in this line
//(or better: we should go to that max if it would pass it)
int sizeExpected = txt.length();
int sizeApplied = currSize + sizeExpected;
if (currSize >= sizeExpected) {
//ok, we already passed what we expected from the indentation, so, let's indent
//to the next 'expected' position...
boolean applied = false;
//handle within parenthesis
if (prevLineTup.o2) {
int len = sizeApplied - sizeExpected;
if (prevExpectedIndent.length() > len) {
command.text = prevExpectedIndent.substring(len);
applied = true;
}
}
if (!applied) {
applyDefaultForTab(command, currSize);
}
} else if (sizeExpected == sizeApplied) {
if (command.length == 0) {
ps.deleteSpacesAfter(command.offset);
}
command.text = txt;
} else if (sizeApplied > sizeExpected) {
ps.deleteSpacesAfter(command.offset);
command.text = txt.substring(0, sizeExpected - currSize);
}
} else {
applyDefaultForTab(command, currSize);
}
} else { //cursorLine == 0
applyDefaultForTab(command, currSize);
}
}
public static void customizeParenthesis(IDocument document, DocumentCommand command,
boolean considerOnlyCurrentLine, IIndentPrefs prefs) throws BadLocationException {
if (prefs.getAutoParentesis()) {
PySelection ps = new PySelection(document, command.offset);
String line = ps.getLine();
if (shouldClose(ps, '(', ')')) {
boolean hasClass = line.indexOf("class ") != -1;
boolean hasClassMethodDef = line.indexOf(" def ") != -1 || line.indexOf("\tdef ") != -1;
boolean hasMethodDef = line.indexOf("def ") != -1;
boolean hasDoublePoint = line.indexOf(":") != -1;
command.shiftsCaret = false;
if (!hasDoublePoint && (hasClass || hasClassMethodDef || hasMethodDef)) {
if (hasClass) {
//command.text = "(object):"; //TODO: put some option in the interface for that
//command.caretOffset = command.offset + 7;
command.text = "():";
command.caretOffset = command.offset + 1;
} else if (hasClassMethodDef && prefs.getAutoAddSelf()) {
String prevLine = ps.getLine(ps.getCursorLine() - 1);
if (prevLine.indexOf("@classmethod") != -1) {
command.text = "(cls):";
command.caretOffset = command.offset + 4;
} else if (prevLine.indexOf("@staticmethod") != -1) {
command.text = "():";
command.caretOffset = command.offset + 1;
} else {
boolean addRegular = true;
if (!considerOnlyCurrentLine) {
//ok, also analyze the scope we're in (otherwise, if we only have the current line
//that's the best guess we can give).
int firstCharPosition = PySelection.getFirstCharPosition(line);
LineStartingScope scopeStart = ps.getPreviousLineThatStartsScope(
PySelection.CLASS_AND_FUNC_TOKENS, false, firstCharPosition);
if (scopeStart != null) {
if (scopeStart.lineStartingScope != null
&& scopeStart.lineStartingScope.indexOf("def ") != -1) {
int iCurrDef = PySelection.getFirstCharPosition(line);
int iPrevDef = PySelection.getFirstCharPosition(scopeStart.lineStartingScope);
if (iCurrDef > iPrevDef) {
addRegular = false;
} else if (iCurrDef == iPrevDef) {
if (scopeStart.lineStartingScope.indexOf("self") == -1) {
//only add self if the one in the same level also has it.
//with a 'gotcha': if it's a classmethod or staticmethod, we
//should still add it.
if (scopeStart.iLineStartingScope <= 0) {
addRegular = false;
} else {
addRegular = false;
int i = scopeStart.iLineStartingScope - 1;
String line2;
do {
line2 = ps.getLine(i).trim();
i--;
if (line2.startsWith("@classmethod")
|| line2.startsWith("@staticmethod")) {
addRegular = true;
break;
}
} while (line2.startsWith("@")); //check all the available decorators...
}
}
}
}
} else {
addRegular = false;
}
}
if (addRegular) {
command.text = "(self):";
command.caretOffset = command.offset + 5;
} else {
command.text = "():";
command.caretOffset = command.offset + 1;
}
}
} else if (hasMethodDef) {
command.text = "():";
command.caretOffset = command.offset + 1;
} else {
throw new RuntimeException(PyAutoIndentStrategy.class.toString()
+ ": customizeDocumentCommand()");
}
} else {
command.text = "()";
command.caretOffset = command.offset + 1;
}
}
}
}
public void customizeNewLine(IDocument document, DocumentCommand command) throws BadLocationException {
prefs = getIndentPrefs();
autoIndentSameAsPrevious(document, command);
if (prefs.getSmartIndentPar()) {
PySelection selection = new PySelection(document, command.offset);
if (selection.getCursorLineContents().trim().length() > 0) {
command.text = autoIndentNewline(document, command.length, command.text, command.offset).o1;
if (PySelection.containsOnlyWhitespaces(selection.getLineContentsToCursor())) {
command.caretOffset = command.offset + selection.countSpacesAfter(command.offset);
}
}
} else {
PySelection selection = new PySelection(document, command.offset);
if (selection.getLineContentsToCursor().trim().endsWith(":")) {
command.text += prefs.getIndentationString();
}
}
}
/**
* Updates the text to the next tab position
* @param command the command to be edited
* @param lineContentsToCursorLen the current cursor position at the current line
*/
private void applyDefaultForTab(DocumentCommand command, int lineContentsToCursorLen) {
IIndentPrefs prefs = getIndentPrefs();
if (prefs.getUseSpaces(true)) {
int tabWidth = getIndentPrefs().getTabWidth();
int mod = (lineContentsToCursorLen + tabWidth) % tabWidth;
command.text = StringUtils.createSpaceString(tabWidth - mod);
} else {
//do nothing (a tab is already a tab)
}
}
/**
* This function makes the else auto-dedent (if available)
* @return the new indent and the number of chars it has been dedented (so, that has to be considered as a shift to the left
* on subsequent things).
*/
public static Tuple<String, Integer> autoDedentAfterColon(IDocument document, DocumentCommand command, String tok,
String[] tokens, IIndentPrefs prefs) throws BadLocationException {
if (prefs.getAutoDedentElse()) {
PySelection ps = new PySelection(document, command.offset);
String lineContents = ps.getCursorLineContents();
if (lineContents.trim().equals(tok)) {
String previousIfLine = ps.getPreviousLineThatStartsWithToken(tokens);
if (previousIfLine != null) {
String ifIndent = PySelection.getIndentationFromLine(previousIfLine);
String lineIndent = PySelection.getIndentationFromLine(lineContents);
String indent = prefs.getIndentationString();
if (lineIndent.length() == ifIndent.length() + indent.length()) {
Tuple<String, Integer> dedented = removeFirstIndent(lineContents, prefs);
ps.replaceLineContentsToSelection(dedented.o1);
command.offset = command.offset - dedented.o2;
return dedented;
}
}
}
}
return null;
}
public static Tuple<String, Integer> autoDedentAfterColon(IDocument document, DocumentCommand command,
IIndentPrefs prefs) throws BadLocationException {
Tuple<String, Integer> ret = null;
if ((ret = autoDedentAfterColon(document, command, "else", PySelection.TOKENS_BEFORE_ELSE, prefs)) != null) {
return ret;
}
if ((ret = autoDedentAfterColon(document, command, "except", PySelection.TOKENS_BEFORE_EXCEPT, prefs)) != null) {
return ret;
}
if ((ret = autoDedentAfterColon(document, command, "finally", PySelection.TOKENS_BEFORE_FINALLY, prefs)) != null) {
return ret;
}
return null;
}
/**
* This function makes the else auto-dedent (if available)
* @return the new indent and the number of chars it has been dedented (so, that has to be considered as a shift to the left
* on subsequent things).
*/
public static Tuple<String, Integer> autoDedentElif(IDocument document, DocumentCommand command, IIndentPrefs prefs)
throws BadLocationException {
return autoDedentAfterColon(document, command, "elif", PySelection.TOKENS_BEFORE_ELIF, prefs);
}
/**
* Create the indentation string after comma and a newline.
*
* @param document
* @param text
* @param offset
* @param selection
* @return Indentation String
* @throws BadLocationException
*/
private String makeSmartIndent(String text, int smartIndent) throws BadLocationException {
if (smartIndent > 0) {
String initial = text;
// Discard everything but the newline from initial, since we'll
// build the smart indent from scratch anyway.
initial = getCharsBeforeNewLine(initial);
// Create the actual indentation string
String indentationString = prefs.getIndentationString();
int indentationSteps = smartIndent / prefs.getTabWidth();
int spaceSteps = smartIndent % prefs.getTabWidth();
StringBuffer b = new StringBuffer(smartIndent);
while (indentationSteps > 0) {
indentationSteps -= 1;
b.append(indentationString);
}
if (prefs.getUseSpaces(true)) {
while (spaceSteps >= 0) {
spaceSteps -= 1;
b.append(" ");
}
}
return initial + b.toString();
}
return text;
}
/**
* @param initial
* @return
*/
private String getCharsBeforeNewLine(String initial) {
int initialLength = initial.length();
for (int i = 0; i < initialLength; i++) {
char theChar = initial.charAt(i);
// This covers all cases I know of, but if there is any platform
// with weird newline then this would need to be smarter.
if (theChar != '\r' && theChar != '\n') {
if (i > 0) {
initial = initial.substring(0, i);
}
break;
}
}
return initial;
}
/**
* Private function which is called when a colon is the command.
*
* The following code will auto-replace colons in function declaractions
* e.g., def something(self): ^ cursor before the end colon
*
* Typing another colon (i.e, ':') at that position will not insert another
* colon
*
* @param document
* @param command
* @throws BadLocationException
*/
private void performColonReplacement(IDocument document, DocumentCommand command) {
PySelection ps = new PySelection(document, command.offset);
int absoluteOffset = ps.getAbsoluteCursorOffset();
int documentLength = ps.getDoc().getLength();
// need to check whether whether we're at the very end of the document
if (absoluteOffset < documentLength) {
try {
char currentCharacter = document.getChar(absoluteOffset);
if (currentCharacter == ':') {
command.text = "";
command.caretOffset = command.offset + 1;
}
} catch (BadLocationException e) {
// should never happen because I just checked the length
throw new RuntimeException(e);
}
}
}
/**
* Private function to call to perform any replacement of braces.
*
* The Eclipse Java editor does this by default, and it is very useful. If
* you try to insert some kind of pair, be it a parenthesis or bracket in
* Java, the character will not insert and instead the editor just puts your
* cursor at the next position.
*
* This function performs the equivalent for the Python editor.
*
* @param document
* @param command if the command does not contain a brace, this function does nothing.
* @throws BadLocationException
*/
private void performPairReplacement(IDocument document, DocumentCommand command) throws BadLocationException {
boolean skipChar = canSkipCloseParenthesis(document, command);
if (skipChar) {
//if we have the same number of peers, we want to eat the char
command.text = "";
command.caretOffset = command.offset + 1;
}
}
/**
* @return true if we should skip a ), ] or }
*/
public boolean canSkipCloseParenthesis(IDocument document, DocumentCommand command) throws BadLocationException {
PySelection ps = new PySelection(document, command.offset);
char c = ps.getCharAtCurrentOffset();
try {
char peer = StringUtils.getPeer(c);
FastStringBuffer doc = new FastStringBuffer(document.get(), 2);
//it is not enough just counting the chars, we have to ignore those that are within comments or literals.
ParsingUtils.removeCommentsWhitespacesAndLiterals(doc, false);
int chars = PyAction.countChars(c, doc);
int peers = PyAction.countChars(peer, doc);
boolean skipChar = chars == peers;
return skipChar;
} catch (NoPeerAvailableException e) {
return false;
} catch (SyntaxErrorException e) {
throw new RuntimeException(e);//not expected!
}
}
/**
* @return true if we should close the opening pair (parameter c) and false if we shouldn't
*/
public static boolean shouldClose(PySelection ps, char c, char peer) throws BadLocationException {
PythonPairMatcher matcher = new PythonPairMatcher(StringUtils.BRACKETS);
String lineContentsFromCursor = ps.getLineContentsFromCursor();
for (int i = 0; i < lineContentsFromCursor.length(); i++) {
char charAt = lineContentsFromCursor.charAt(i);
if (!Character.isWhitespace(charAt)) {
if (charAt == ',') {
break;
}
if (StringUtils.isClosingPeer(charAt)) {
break;
}
return false;
}
}
//Ok, we have to analyze the current context and see if each closing peer
//in this context has a match. If one doesn't, we won't close it.
LineStartingScope nextLineThatStartsScope = ps.getNextLineThatStartsScope();
int lineStartingNextScope;
if (nextLineThatStartsScope == null) {
lineStartingNextScope = Integer.MAX_VALUE;
} else {
lineStartingNextScope = nextLineThatStartsScope.iLineStartingScope;
}
int closingPeerLine;
int closingPeerFoundAtOffset = ps.getAbsoluteCursorOffset() - 1; //start to search at the current position
do {
//closingPeerFoundAtOffset doesn't need +1 here as it's already added in the matcher.
closingPeerFoundAtOffset = matcher.searchForClosingPeer(closingPeerFoundAtOffset, c, peer, ps.getDoc());
if (closingPeerFoundAtOffset == -1) {
//no more closing peers there, ok to go
return true;
}
//the +1 is needed because we match closing ones that are right before the current cursor
IRegion match = matcher.match(ps.getDoc(), closingPeerFoundAtOffset + 1);
if (match == null) {
//we don't have a match for a close, so, this open is that match.
return false;
}
try {
closingPeerLine = ps.getDoc().getLineOfOffset(closingPeerFoundAtOffset);
} catch (Exception e) {
break;
}
} while (lineStartingNextScope > closingPeerLine);
return true;
}
/**
* Return smart indent amount for new line. This should be done for
* multiline structures like function parameters, tuples, lists and
* dictionaries.
*
* Example:
*
* a=foo(1, #
*
* We would return the indentation needed to place the caret at the #
* position.
*
* @param document The document
* @param offset The document offset of the last character on the previous line
* @param ps
* @return indent, or -1 if smart indent could not be determined (fall back to default)
* and a boolean indicating if we're inside a parenthesis
*/
public static Tuple<Integer, Boolean> determineSmartIndent(int offset, IDocument document, IIndentPrefs prefs)
throws BadLocationException {
PythonPairMatcher matcher = new PythonPairMatcher(StringUtils.BRACKETS);
int openingPeerOffset = matcher.searchForAnyOpeningPeer(offset, document);
if (openingPeerOffset == -1) {
return new Tuple<Integer, Boolean>(-1, false);
}
final IRegion lineInformationOfOffset = document.getLineInformationOfOffset(openingPeerOffset);
//ok, now, if the opening peer is not on the line we're currently, we do not want to make
//an 'auto-indent', but keep the current indentation level
boolean openingPeerIsInCurrentLine = PySelection.isInside(offset, lineInformationOfOffset);
int len = -1;
String contents = "";
if (prefs.getIndentToParLevel()) {
//now, a catch, if we didn't change the indent level, we've to indent in the same level
//as the previous line, as this means that the user 'customized' the indent level at this place.
PySelection ps = new PySelection(document, offset);
String lineContentsToCursor = ps.getLineContentsToCursor();
if (!openingPeerIsInCurrentLine && !StringUtils.hasUnbalancedClosingPeers(lineContentsToCursor)) {
try {
char openingChar = document.getChar(openingPeerOffset);
int closingPeerOffset = matcher.searchForClosingPeer(openingPeerOffset, openingChar,
StringUtils.getPeer(openingChar), document);
if (closingPeerOffset == -1 || offset <= closingPeerOffset) {
return new Tuple<Integer, Boolean>(-1, true); // True because we're inside a parens
}
} catch (Exception e) {
Log.log(e);
//Something unexpected happened... (document changed?)
return new Tuple<Integer, Boolean>(-1, true); // True because we're inside a parens
}
}
//now, there's a little catch here, if we are in a line with an opening peer,
//we have to choose whether to indent to the opening peer or a little further
//e.g.: if the line is
//method( self <<- a new line here should indent to the start of the self and not
//to the opening peer.
if (openingPeerIsInCurrentLine && openingPeerOffset < offset) {
String fromParToCursor = document.get(openingPeerOffset, offset - openingPeerOffset);
if (fromParToCursor.length() > 0 && fromParToCursor.charAt(0) == '(') {
fromParToCursor = fromParToCursor.substring(1);
if (!PySelection.containsOnlyWhitespaces(fromParToCursor)) {
final int firstCharPosition = PySelection.getFirstCharPosition(fromParToCursor);
openingPeerOffset += firstCharPosition;
}
}
}
int openingPeerLineOffset = lineInformationOfOffset.getOffset();
len = openingPeerOffset - openingPeerLineOffset;
contents = document.get(openingPeerLineOffset, len);
} else {
if (!openingPeerIsInCurrentLine) {
return new Tuple<Integer, Boolean>(-1, true);
}
//ok, don't indent to parenthesis level: Just add the regular indent level
int line = document.getLineOfOffset(openingPeerOffset);
final String indent = prefs.getIndentationString();
contents = PySelection.getLine(document, line);
contents = PySelection.getIndentationFromLine(contents);
StringBuffer sb = new StringBuffer();
//Create the string for the indent level we want.
for (int i = 0; i < prefs.getIndentAfterParWidth(); i++) {
sb.append(indent);
}
contents += sb.substring(0, sb.length() - 1); //we have to make it -1 (that's what the smartindent expects)
len = contents.length();
}
//add more spaces for each tab
for (int i = 0; i < contents.length(); i++) {
if (contents.charAt(i) == '\t') {
len += prefs.getTabWidth() - 1;
}
}
return new Tuple<Integer, Boolean>(len, true);
}
public void setBlockSelection(boolean blockSelection) {
this.blockSelection = blockSelection;
}
public void customizeParenthesis(IDocument doc, DocumentCommand docCmd) throws BadLocationException {
PyAutoIndentStrategy.customizeParenthesis(doc, docCmd, true, this.getIndentPrefs());
}
/**
* Empty document (should not be written to).
*/
IDocument EMPTY_DOCUMENT = new Document();
public String convertTabs(String cmd) {
DocCmd newStr = new DocCmd(0, 0, cmd);
getIndentPrefs().convertToStd(EMPTY_DOCUMENT, newStr);
cmd = newStr.text;
return cmd;
}
}